/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.sharding.client.listener;

import com.alibaba.druid.pool.DruidDataSource;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.lvyh.lightframe.sharding.client.context.MultiDataSourceHolder;
import com.lvyh.lightframe.sharding.client.datasource.DynamicRoutingDataSource;
import com.lvyh.lightframe.sharding.client.util.ReflectionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Data source change event subscriber
 *
 * @author lvyh 2021/11/02.
 */
@Service
public class NacosDatasourceUpdateListener implements ApplicationListener<RefreshDatasourceEvent> {
    private static Logger logger = LoggerFactory.getLogger(NacosDatasourceUpdateListener.class);
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * Store registered default data source
     */
    private DataSource defaultDatasource = null;
    /**
     * Store registered other data sources
     */
    private Map<Object, Object> otherDataSources = new HashMap<>();

    @Override
    public void onApplicationEvent(RefreshDatasourceEvent refreshDatasourceEvent) {
        //logger.info("Refresh data source: " + refreshDatasourceEvent.getSource());
        DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) applicationContext.getBean("datasource");

        DataSource defaultDatasource = null;

        String data = (String) refreshDatasourceEvent.getSource();
        List<Map> multiDatasourceConfigList = buildMultiDatasourceConfigList(data);

        if (!CollectionUtils.isEmpty(multiDatasourceConfigList)) {
            for (Map config : multiDatasourceConfigList) {
                DataSource tempDatasource = createDataSource(config);

                String datasourceName = config.get("name").toString();
                if (Objects.equals(datasourceName, "master")) {
                    defaultDatasource = tempDatasource;
                } else {
                    otherDataSources.put(datasourceName, tempDatasource);
                }

                // Data source context, which is used to manage data sources and record registered data source names.
                List<String> multiDataSources = MultiDataSourceHolder.getMultiDataSources();
                if (!multiDataSources.contains(datasourceName)) {
                    MultiDataSourceHolder.getMultiDataSources().add(datasourceName);
                    logger.info("Register data source successfully:{}", datasourceName);
                }
            }
        }

        try {
            Object defaultObject = ReflectionUtil.getFieldValue(dynamicRoutingDataSource, "defaultTargetDataSource", true);
            if (defaultObject != null) {
                DruidDataSource oldDefaultDataSource = (DruidDataSource) defaultObject;
                //Dynamic refresh configuration
                oldDefaultDataSource.restart();
                BeanUtils.copyProperties(defaultDatasource, oldDefaultDataSource);
                oldDefaultDataSource.init();
            }

            Object object = ReflectionUtil.getFieldValue(dynamicRoutingDataSource, "targetDataSources", true);
            if (object != null) {
                Map<Object, Object> oldTargetDataSources = (Map<Object, Object>) object;
                for (Map.Entry<Object, Object> entry : oldTargetDataSources.entrySet()) {
                    Object value = entry.getValue();
                    String key = (String) entry.getKey();
                    DruidDataSource newDataSource = (DruidDataSource) otherDataSources.get(key);
                    if (value instanceof DruidDataSource) {
                        DruidDataSource oldDataSource = (DruidDataSource) value;
                        oldDataSource.restart();
                        BeanUtils.copyProperties(newDataSource, oldDataSource);
                        oldDataSource.init();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void dynamicallyRefreshBean() {
        //Get beanfactory
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

        //Create bean information
        GenericBeanDefinition define = new GenericBeanDefinition();
        // Set bean type
        define.setBeanClass(DynamicRoutingDataSource.class);

        // Parameters to be injected
        MutablePropertyValues mpv = define.getPropertyValues();

        // Add the default data source to avoid the situation that the key does not exist and no data source is available.
        mpv.add("defaultTargetDataSource", defaultDatasource);

        // Add another data source
        mpv.add("targetDataSources", otherDataSources);

        // Dynamic delete
        defaultListableBeanFactory.removeBeanDefinition("datasource");

        //Dynamically register bean.
        defaultListableBeanFactory.registerBeanDefinition("datasource", define);
    }

    private DataSource createDataSource(Map properties) {
        DruidDataSource dataSource = new DruidDataSource();

        String initialSize = (String) properties.get("initialSize");
        if (!StringUtils.isEmpty(initialSize)) {
            dataSource.setInitialSize(Integer.parseInt(initialSize));
        }
        String maxActive = (String) properties.get("maxActive");
        if (!StringUtils.isEmpty(maxActive)) {
            dataSource.setMaxActive(Integer.parseInt(maxActive));
        }
        String maxWait = (String) properties.get("maxWait");
        if (!StringUtils.isEmpty(maxWait)) {
            dataSource.setMaxWait(Integer.parseInt(maxWait));
        }
        String name = (String) properties.get("name");
        if (!StringUtils.isEmpty(name)) {
            dataSource.setName(name);
        }
        String url = (String) properties.get("url");
        if (!StringUtils.isEmpty(url)) {
            dataSource.setUrl(url);
        }
        String username = (String) properties.get("username");
        if (!StringUtils.isEmpty(username)) {
            dataSource.setUsername(username);
        }
        String password = (String) properties.get("password");
        if (!StringUtils.isEmpty(password)) {
            dataSource.setPassword(password);
        }
        dataSource.setTestOnBorrow(true);
        dataSource.setValidationQuery("SELECT 1");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    private static void parseLine(Multimap<String, String> multiMap, String line) {
        String[] arr = line.split("=");
        String datasourceConfKey = arr[0];
        String datasourceValue = arr[1];
        StringBuffer buffer = new StringBuffer();
        if (arr.length > 2) {
            for (int i = 1; i < arr.length; i++) {
                if (i < arr.length - 1) {
                    buffer.append(arr[i]).append("=");
                } else
                    buffer.append(arr[i]);
            }
            datasourceValue = buffer.toString();
        }
        int lastIndex = datasourceConfKey.lastIndexOf(".");
        String datasourceKey = datasourceConfKey.substring(lastIndex + 1, datasourceConfKey.length());
        String key = datasourceConfKey.substring(0, lastIndex);
        multiMap.put(key, datasourceKey + "=" + datasourceValue);
    }

    private static List<Map> buildMultiDatasourceConfigList(String content) {
        List<Map> multiDatasourceConfigList = new ArrayList<>();
        try {
            Multimap<String, String> datasourceMultiMap = ArrayListMultimap.create();
            BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(content.getBytes(Charsets.UTF_8)), Charsets.UTF_8));
            String line;
            while ((line = br.readLine()) != null) {
                if (!line.trim().equals("")) {
                    parseLine(datasourceMultiMap, line);
                }
            }

            Map<String, Collection<String>> datasourceMap = datasourceMultiMap.asMap();
            for (Map.Entry<String, Collection<String>> entry : datasourceMap.entrySet()) {
                Collection<String> valueList = entry.getValue();
                if (!CollectionUtils.isEmpty(valueList)) {
                    Map map = new HashMap();
                    for (String item : valueList) {
                        String[] arr = item.split("=");
                        String datasourceValue = arr[1];
                        StringBuffer buffer = new StringBuffer();
                        if (arr.length > 2) {
                            for (int i = 1; i < arr.length; i++) {
                                if (i < arr.length - 1) {
                                    buffer.append(arr[i]).append("=");
                                } else
                                    buffer.append(arr[i]);
                            }
                            datasourceValue = buffer.toString();
                        }
                        map.put(arr[0], datasourceValue);
                    }
                    multiDatasourceConfigList.add(map);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return multiDatasourceConfigList;
    }
}
